package io.github.lee.core.vm

import android.app.Application
import android.content.Context
import android.util.Log
import androidx.annotation.StringRes
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.viewModelScope
import io.github.lee.core.ui.R
import io.github.lee.core.vm.data.UiEvent
import io.github.lee.core.vm.data.UiState
import io.github.lee.core.vm.err.ResultDataNullException
import io.github.lee.core.vm.err.ViewModelException
import io.github.lee.core.vm.lifecycle.ViewModelLifecycle
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.InterruptedIOException
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext


open class BaseViewModel(application: Application) : AndroidViewModel(application),
    ViewModelLifecycle {
    protected val mContext: Context by lazy {
        getApplication()
    }
    private val _uiState = MutableSharedFlow<UiState>()
    private val _uiEvent = MutableSharedFlow<UiEvent>()

    private val _softKeyboardFlow = MutableSharedFlow<Boolean>()


    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
    }
    //===Desc:======================================================================================

    protected fun launch(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> Unit
    ) = viewModelScope.launch(context, start, block)

    protected fun <T> launchResult(
        block: suspend CoroutineScope.() -> T,
        error: (suspend (t: ViewModelException?) -> Unit)? = null,
        success: suspend CoroutineScope.(data: T) -> Unit
    ) {
        try {
            val handle = CoroutineExceptionHandler { _, throwable ->
                throwable.printStackTrace()
                launch {
                    if (throwable is InterruptedIOException) {
                        error?.invoke(
                            ViewModelException(
                                500,
                                getString(R.string.ui_txt_network_error)
                            )
                        )
                    } else {
                        error?.invoke(ViewModelException(500, throwable.message))
                    }
                }
            }
            launch(handle + Dispatchers.IO) {
                val result = withContext(Dispatchers.IO) {
                    block()
                }
                if (null == result) {
                    launch(Dispatchers.Main) {
                        error?.invoke(ResultDataNullException(mContext))
                    }
                } else {
                    launch(Dispatchers.Main) {
                        success(result)
                    }
                }
            }

        } catch (e: Exception) {
            launch(Dispatchers.Main) {
                error?.invoke(ViewModelException(500, e.message))
            }
        }
    }

    //===Desc:======================================================================================

    fun emitUiState(state: UiState) {
        launch(Dispatchers.IO) {
            _uiState.emit(state)
        }
    }

    open fun collectUiState(collector: FlowCollector<UiState>) {
        launch {
            _uiState.collect(collector)
        }
    }

    open fun collectSoftKeyboard(collector: FlowCollector<Boolean>) {
        launch {
            _softKeyboardFlow.collect(collector)
        }
    }

    fun hideSoftKeyboard() {
        launch(Dispatchers.IO) {
            _softKeyboardFlow.emit(false)
        }
    }

    fun showSoftKeyboard() {
        launch(Dispatchers.IO) {
            _softKeyboardFlow.emit(true)
        }
    }

    fun loading(tip: String? = "") =
        emitUiState(UiState.Loading(tip))

    fun success() =
        emitUiState(UiState.Success)

    fun empty(e: ViewModelException? = null) {
        emitUiState(UiState.Empty(e))
    }

    fun error(e: ViewModelException? = null) =
        emitUiState(UiState.Error(e))

    private fun emitUiEvent(event: UiEvent) {
        launch(Dispatchers.IO) {
            _uiEvent.emit(event)
        }
    }


    fun showProgress(tip: String? = null, canDismiss: Boolean? = true) {
        emitUiEvent(UiEvent.ShowProgress(tip, canDismiss))
    }

    fun hideProgress(runnable: Runnable? = null) {
        emitUiEvent(UiEvent.HideProgress(runnable))
    }

    fun toast(msg: String? = null, isLong: Boolean = false) {
        emitUiEvent(UiEvent.Toast(msg, isLong))
    }

    fun toast(@StringRes resId: Int? = null, isLong: Boolean = false) {
        if (null != resId) {
            emitUiEvent(UiEvent.Toast(getString(resId), isLong))
        }
    }

    fun toast(exception: ViewModelException, isLong: Boolean = false) {
        toast(exception.format(mContext), isLong)
    }


    fun refreshSuccess(hasMore: Boolean? = true) {
        emitUiEvent(UiEvent.RefreshSuccess(hasMore))
    }

    fun refreshFail(e: ViewModelException? = null) {
        emitUiEvent(UiEvent.RefreshFail(e))
    }

    fun loadMoreSuccess(hasMore: Boolean = true) {
        emitUiEvent(UiEvent.LoadMoreSuccess(hasMore))
    }

    fun loadMoreFail(e: ViewModelException? = null) {
        emitUiEvent(UiEvent.LoadMoreFail(e))
    }


    fun collectUiEvent(collector: FlowCollector<UiEvent>) {
        launch {
            _uiEvent.collect(collector)
        }
    }


    //===Desc:======================================================================================
    protected fun getString(@StringRes resId: Int): String =
        getApplication<Application>().getString(resId)


    protected fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String =
        getApplication<Application>().getString(resId, *formatArgs)

}